原理就是这么简单 用Python搭建神经网络

不做调包侠!

之前我在某公开课上讲述了“不调包,仅用Python如何四步搭建神经网络”。
我发现各位小伙伴们对原理的渴求非常强烈!说明大家在“不做调包侠”上非常重视,我也非常开心。
这篇文章也是“原理就是这么简单”的系列文章之一,希望通过文章的方式将原理描述的更加丰满!帮助更多想学习深度学习的朋友!

对比Sklearn中的线性回归

学过机器学习的朋友都知道Sklearn是非常重要的工具包,很多复杂的机器学习模型也许用sklearn几行代码就能搞定,
举个例子比如线性回归,我们可以构建如下的线性回归对象训练和预测一气呵成:

1
2
3
linreg = LinearRegression()
linreg.fit(train_features,train_targets)
y_pred = linreg.predict(val_features)

发现使用非常简单,仅仅三行代码就能得到线性回归的结果(y_pred
如果使用的案例是波士顿房价的数据集,那相应可以得到如下的预测结果:
Boston_Housing_predict.PNG

预测结果还不错。

那如何构建一个类似Skleran.LinearRegression 的神经网络工具包呢?
接下来我就要将数学原理转换为代码。

##Python搭建神经网络完成回归工作
终于到了本篇文章的重点,我会用数学原理和python代码详细讲解如何构建如下结构的神经网络:
nn.png

介绍网络

大家可以看到这个神经网络分别有

  • Input Layer
  • Hidden Layer
  • Output Layer

这个是这个网络的基本结构,在进行下面的讲解之前需要在这里声明一下,

既然是Layer 层 , 都会有这个层本身输入和输出,就好比一个净水过滤器,传进去的是脏水,传出来的是净水,那么神经网络的各个层也有这个性质。

1, Input Layer 值得注意的地方是Input Layer 只有输出,传入的就是数据特征,如果使用的是预测房价的案例传入的就是(面积,朝向,地段,etc..)。
公式中会表示成X

2, Hidden Layer 有输入和输出两个概念了,就好比净水器。那么针对Hidden Layer净水器的过滤网是什么呢? 其实就是激活函数(这个后面我会详细讲解)。
公式中会表示成h_input/h_out

3, Output Layer 也有输入和输出两个概念,只不过这次我搭建的是处理回归模型的网络,所以其输入和输出是一样的值。
公式中会表示成O_input/O_out

4, 链接层与层之间的是权重矩阵。
公式中会表示成W_i_h/ W_h_o

BP算法数学原理和代码实现

其实 BP 算法的全名为”Error BackPropagation 误差反向传播”
但是其实BP算法的流程分为三个部分:
一为前向传播求误差
二为反向传播求梯度
三为通过梯度更新权重

可能大家会对这三个部分晦涩难懂,其实我举个不恰当的例子你就能明白。
比如小明想去学习自由泳,他没有教练,不过好在有一个泳池他可以无限次的在里面尝试。
然后小明开始自学:

  • 步骤一,他脑子里想了A,B,C 三套动作准备在水中尝试,然后直接跳入水中瞎扑腾,看看到底是落水还是前进。
  • 步骤二,在水中他认真体会各种动作对于游泳的影响,发现使用C动作他还能扑腾一会,但是使用A,B动作,瞬间落水。
  • 步骤三,他上了岸,然后仔细总结刚才的C动作并加以改进,并且尝试去除自己的A,B动作。

然后再次跳入水中。

经过n 次的刻苦训练,小明练成了自由泳。

其实这个流程就好比BP算法的流程。
步骤一就像前向传播,带着一些不靠谱的动作相当于初始权重矩阵
步骤二就像于反向传播,仔细体验哪个动作更加有效这相当于求权重的梯度
步骤三就像于梯度更新,更新自己的动作相当于更新权重矩阵

下面我就要仔细用数学推导BP算法:
首先来学习一下数学基础:
base.JPG
Loss function 不用过多解释,了解机器学习的都知道这是MSE。
Sigmoid 就是刚才提到的过滤器也就是激活函数。

前向传播:
前向传播.JPG
代码如下:
代码中final_outputs 就是 O_out

1
2
3
4
hidden_inputs = np.dot(X,self.input_hidden_weight)
hidden_outputs = self.sigmoid(hidden_inputs)
final_inputs = np.dot(hidden_outputs,self.hidden_output_weight)
final_outputs = final_inputs

反向传播:
基于链式求导法则如下:
反向传播.JPG
关注O_input_error_term, h_input_error_term
因为这两项是关于权重矩阵W_i_h和W_h_o的函数。
代码如下:

1
2
3
4
final_output_error = y-final_outputs
final_input_error_term = final_output_error*1
hidden_output_error = np.dot(self.hidden_output_weight, final_input_error_term)
hidden_input_error_term = hidden_output_error*hidden_outputs*(1-hidden_outputs)

梯度更新:
梯度更新.JPG
代码如下:
代码中update_x_x 就是权重的梯度

1
2
update_i_h += np.dot(X_item,hidden_error_term.T)
update_h_o += np.dot(hidden_outputs,output_error_term.T)

到了这里你已经学会了BP算法的精华了,但是如果想构造一个可以使用的神经网络还差一点点,是什么呢?
下面我会根据代码给你讲解。

Python 四步构造NeuralNetwork

想要构造一个类似Sklearn的方便使用的神经网络,我们需要四步

Step1:
初始化:

  • 构造sigmoid 激活函数
  • 构造mse 用于求损失
  • 设置学习率,以及网络节点
  • 初始化两个权重矩阵

Step2

  • 实现前向传播
  • 实现predect函数基于前向传播

Step3

  • 实现反向传播

整个代码如下:
···
class NeuralNetwork(object):
def init(self, input_units, hidden_units = 4, output_units = 1, learning_rate = 0.01):

    self.sigmoid = lambda x : 1/(1+np.exp(-x))
    self.mean_squared_error = lambda y_true, y_pred: np.mean((y_true-y_pred)**2)

    self.input_units = input_units
    self.hidden_units = hidden_units
    self.output_units = output_units

    self.learning_rate = learning_rate

    np.random.seed(1119)

    self.input_hidden_weight = np.random.randn(self.input_units,self.hidden_units)

    self.hidden_output_weight = np.random.randn(self.hidden_units,self.output_units)

def __forward__(self, X):
    ''' forward pass through the neural network with X

        Arguments
        ---------
        X: 2D array
        features

        Returns
        -------
        hidden_outputs: 1D array
        final_outputs: 1D array

    '''
    hidden_inputs = np.dot(X,self.input_hidden_weight)

    hidden_outputs = self.sigmoid(hidden_inputs)

    final_inputs = np.dot(hidden_outputs,self.hidden_output_weight)

    final_outputs = final_inputs

    return hidden_outputs,final_outputs

def __backward__(self, y, hidden_outputs, final_outputs):
    ''' backward pass through the neural network with X

        Arguments
        ---------
        X: 2D array
        features

        Returns
        -------
        hidden_input_error_term: 1D array
        final_input_error_term: 1D array

    '''
    final_output_error = y-final_outputs

    final_input_error_term = final_output_error*1

    hidden_output_error = np.dot(self.hidden_output_weight, final_input_error_term)

    hidden_input_error_term = hidden_output_error*hidden_outputs*(1-hidden_outputs)

    return hidden_input_error_term,final_input_error_term

def fit(self, X, y):
    ''' fit(X, y) method of NeuralNetwork

        Parameters
        ----------
        X : 2D array
        Training data

        y : 1D array
        Target values

        Returns
        -------
        void

    '''
    n_records = X.shape[0]
    for X_item, y_item in zip(X, y):
        hidden_outputs, final_outputs = self.__forward__(X_item)
        hidden_error_term, output_error_term = self.__backward__(y_item,hidden_outputs,final_outputs)
        update_i_h += np.dot(X_item,hidden_error_term.T)
        update_h_o += np.dot(hidden_outputs,output_error_term.T)

    self.hidden_output_weight += self.learning_rate * (update_h_o / n_records)
    self.input_hidden_weight += self.learning_rate * (update_i_h / n_records)


def predict(self, X):
    ''' predict(X) method of NeuralNetwork

        Arguments
        ---------
        X: 2D array
        features

        Returns
        -------
        outputs: 1D array
        predicted values
    '''
    hidden_outputs, final_outputs = self.__forward__(X)

    return final_outputs

总结

对比了一下自己构造的神经网络和线性回归,预测波士顿房价的案例可以得到如下结果:
线性回归VS神经网络.PNG

可以明显看出神经网络的性能更好一些。

并且神经网络可以做回归和分类两种任务,只要将Output layer 加一个Sigmoid 即可实现二分类,加一个Softmax 又可以实现多分类(可参考另一篇 原理就是这么简单 Softmax 分类)

理论上来说神经网络,有足够都的数据有足够多的层数和节点数,可以拟合任何函数,这也是神经网络的强大之处。
也是如今的AI 时代深度学习作为爆发技术的主要原因。

你也可以自己尝试实现,如果有问题,欢迎给我留言